﻿using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using System.Collections.Observable;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Collections.Observable.ExtendedEventArgs;
using System.ComponentModel;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

namespace ObservableUnitTests
{
    [TestClass]
    public class ObservableCollectionBaseTest
    {
        ObservableCollectionBase<int, ICollection<int> > ObservableCollection;
        List<NotifyCollectionChangedEventArgs> ObservableEvents;
        
        
        [TestInitialize]
        public void TestInitialize()
        {
            Collection<int> collection =  new Collection<int> { 0, 1, 2, 3, 4, 5 };
            ObservableCollection = new ObservableCollectionBase<int, ICollection<int> > (collection);

            ObservableEvents = new List<NotifyCollectionChangedEventArgs>();

            ObservableCollection.CollectionChanged += (s, e) => { ObservableEvents.Add(e); };
        }
        
        [TestMethod]
        public void AddOne()
        {
            int value = 123;
            ObservableCollection.Add(value);
            Assert.AreEqual(1, ObservableEvents.Count);

            NotifyCollectionChangedEventArgs args = ObservableEvents[0];

            Assert.AreEqual(NotifyCollectionChangedAction.Add, args.Action);
            Assert.IsNotNull(args.NewItems);
            Assert.IsNull(args.OldItems);

            Assert.AreEqual(args.NewStartingIndex, -1);
            Assert.AreEqual(args.OldStartingIndex, -1);

            int addedValue = (int)args.NewItems[0];
            Assert.AreEqual(value, addedValue);
        }

        [TestMethod]
        public void RemoveOne()
        {
            int value = ObservableCollection.First();
            ObservableCollection.Remove(value);

            Assert.AreEqual(1, ObservableEvents.Count);

            NotifyCollectionChangedEventArgs args = ObservableEvents[0];

            Assert.AreEqual(NotifyCollectionChangedAction.Remove, args.Action);
            Assert.IsNull(args.NewItems);
            Assert.IsNotNull(args.OldItems);

            Assert.AreEqual(args.NewStartingIndex, -1);
            Assert.AreEqual(args.OldStartingIndex, -1);


            int removedValue = (int)args.OldItems[0];
            Assert.AreEqual(value, removedValue);
        }

        [TestMethod]
        public void Clear()
        {
            List<int> cleared = ObservableCollection.ToList();
            ObservableCollection.Clear();

            Assert.AreEqual(1, ObservableEvents.Count);


            //verify clear event
            NotifyCollectionChangedEventArgs removeArgs = ObservableEvents[0];
            Assert.AreEqual(NotifyCollectionChangedAction.Reset, removeArgs.Action);
            Assert.IsNull(removeArgs.NewItems);
            Assert.IsNull(removeArgs.OldItems);
            Assert.IsNotNull(removeArgs.GetResetedItems());

            Assert.AreEqual(removeArgs.NewStartingIndex, -1);
            Assert.AreEqual(removeArgs.OldStartingIndex, -1);

            List<int> removed = new List<int>();
            foreach (var a in removeArgs.GetResetedItems())
            {
                removed.Add((int)a);
            }

            Assert.AreEqual(cleared.Count, removed.Count);

            cleared.Sort();
            removed.Sort();
            Assert.IsTrue(Enumerable.SequenceEqual(cleared, removed));
            
            
        }

        [TestMethod]
        public void CollectionTransaction_MergeAdd()
        {
            int value = 100;
            int insertCount = 10;

            using (var transaction = ObservableCollection.BeginTransaction())
            {
                for (int i = 0; i < insertCount; i++)
                {
                    ObservableCollection.Add(value);
                }

                //no events fired yet
                Assert.AreEqual(0, ObservableEvents.Count);
            }

            //verify
            Assert.AreEqual(1, ObservableEvents.Count);

            NotifyCollectionChangedEventArgs args = ObservableEvents[0];

            Assert.AreEqual(NotifyCollectionChangedAction.Add, args.Action);
            Assert.IsNotNull(args.NewItems);
            Assert.IsNull(args.OldItems);

            Assert.AreEqual(args.NewStartingIndex, -1);
            Assert.AreEqual(args.OldStartingIndex, -1);

            Assert.AreEqual(insertCount, args.NewItems.Count);
            foreach (int v in args.NewItems)
            {
                Assert.AreEqual(value, v);
            }
        }

        [TestMethod]
        public void CollectionTransaction_MergeRemove()
        {
            List<int> removedItems = new List<int>();

            using (var transaction = ObservableCollection.BeginTransaction())
            {
                while(ObservableCollection.Count > 0)
                {
                    removedItems.Add(ObservableCollection.First());
                    ObservableCollection.Remove(ObservableCollection.First());                    
                }

                //no events fired yet
                Assert.AreEqual(0, ObservableEvents.Count);
            }

            //verify
            Assert.AreEqual(1, ObservableEvents.Count);

            NotifyCollectionChangedEventArgs args = ObservableEvents[0];

            Assert.AreEqual(NotifyCollectionChangedAction.Remove, args.Action);
            Assert.IsNull(args.NewItems);
            Assert.IsNotNull(args.OldItems);

            Assert.AreEqual(args.NewStartingIndex, -1);
            Assert.AreEqual(args.OldStartingIndex, -1);

            Assert.AreEqual(removedItems.Count, args.OldItems.Count);

            for (int i = 0; i < removedItems.Count; i++)
            {
                Assert.AreEqual(removedItems[i], args.OldItems[i]);
            }
        }

        [TestMethod]
        public void CollectionTransaction_MultiMerge()
        {
            int addCount = 10;
            int removeCount = 10;
            int expectedEventCount = 0;

            using (var transaction = ObservableCollection.BeginTransaction())
            {
                //add 
                for(int i = 0 ; i < addCount; i++)
                {
                    ObservableCollection.Add(0);
                }
                expectedEventCount++;

                //remove
                for (int i = 0; i < removeCount; i++)
                {
                    ObservableCollection.Remove(ObservableCollection.First()); 
                }
                expectedEventCount++;

                //add 
                for (int i = 0; i < addCount; i++)
                {
                    ObservableCollection.Add(0);
                }
                expectedEventCount++;

                //reset
                ObservableCollection.Clear();
                expectedEventCount += 1; //1 for remove all and 1 for reset event
                                

                //no events fired yet
                Assert.AreEqual(0, ObservableEvents.Count);
            }

            //verify
            Assert.AreEqual(expectedEventCount, ObservableEvents.Count);
        }

        [TestMethod]
        public void PropertyChanged_SubscribeFromInitialList()
        {
            List<NotifyPropertyChange> initialList = new List<NotifyPropertyChange>();
            for (int i = 0; i < 100; i++)
            {
                //add items with current number as their names 
                initialList.Add(new NotifyPropertyChange( i.ToString() ));
            }

            //create observable collection
            ObservableCollectionBase<NotifyPropertyChange, List<NotifyPropertyChange>> observableCollection =
                   new ObservableCollectionBase<NotifyPropertyChange, List<NotifyPropertyChange>>(initialList);
            
            //all PropertyChanged fired events list
            List<string> propertyNames = new List<string>();            
            observableCollection.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName);
            
            //run through all items and trigger PropertyChange events for each.
            foreach (NotifyPropertyChange o in observableCollection)
            {
                o.Notify();
            }

            //verify events were fired correctly
            Assert.AreEqual(initialList.Count, propertyNames.Count);
            for (int i = 0; i < initialList.Count; i++)
            {
                Assert.AreEqual(initialList[i].Name, propertyNames[i]);
            }            
        }

        [TestMethod]
        public void PropertyChanged_SubscirbeFromAdd()
        {
            List<NotifyPropertyChange> notifyList = new List<NotifyPropertyChange>();
           
            ObservableCollectionBase<NotifyPropertyChange, List<NotifyPropertyChange>> observableCollection =
                   new ObservableCollectionBase<NotifyPropertyChange, List<NotifyPropertyChange>>(notifyList);

            //add items directly to observableDict
            for (int i = 0; i < 100; i++)
            {
                observableCollection.Add(new NotifyPropertyChange(i.ToString()));
            }

            //add list that holds all fired PropertyChanged events
            List<string> propertyNames = new List<string>();
            observableCollection.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName);

            //run through all items and trigger PropertyChange events for each.
            foreach (NotifyPropertyChange o in observableCollection)
            {
                o.Notify();            
            }

            //verify that events were fired correctly
            Assert.AreEqual(notifyList.Count, propertyNames.Count);
            for (int i = 0; i < notifyList.Count; i++)
            {
                Assert.AreEqual(notifyList[i].Name, propertyNames[i]);
            }
        }

        [TestMethod]
        public void PropertyChanged_UnsubscribeFromRemove()
        {
            //create signle item under test
            NotifyPropertyChange item = new NotifyPropertyChange("item");

            //create initial list
            List<NotifyPropertyChange> initialList = new List<NotifyPropertyChange>();
            initialList.Add(item);
            
            //create observable collection
            ObservableCollectionBase<NotifyPropertyChange, List<NotifyPropertyChange>> observableCollection =
                   new ObservableCollectionBase<NotifyPropertyChange, List<NotifyPropertyChange>>(initialList);
            
            //create list that holds all fired PropertyChanged events
            List<string> propertyNames = new List<string>();
            observableCollection.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName);
            
            //remove item under test from list
            observableCollection.Remove(item);

            //run through all items and trigger PropertyChange events for each.
            foreach (NotifyPropertyChange o in observableCollection)
            {
                o.Notify();
            }

            //verify no event were fired
            Assert.AreEqual(0, propertyNames.Count);
        }


        class Counter
        {
            public int count = 0;
        }

        [TestMethod]
        public void PropertyChangedSubscription()
        {
            int addItemCount = 3;
            var list = new List<EventSerializedPropertyChanged>();

            for (int i = 0; i < addItemCount; i++)
            {
                list.Add(new EventSerializedPropertyChanged());
            }

            var observableCollection = 
                new ObservableCollectionBase<EventSerializedPropertyChanged, List<EventSerializedPropertyChanged>>(list);

            Counter callbackCount = new Counter();

            observableCollection.PropertyChanged += (s,e)=> callbackCount.count++;

            //verify whenever item notifies PropertyEvent from collection is fired.
            foreach (var item in observableCollection)
            {
                callbackCount.count = 0;
                item.Notify();
                Assert.AreEqual(callbackCount.count, 1);
            }
        }

        /// <summary>
        /// This class serialized all subscribers for PropertyChanged.
        /// </summary>         
        [Serializable]
        class EventSerializedPropertyChanged : INotifyPropertyChanged
        {
            public event PropertyChangedEventHandler PropertyChanged;

            public void Notify()
            {
                PropertyChangedEventHandler handler = PropertyChanged;
                if (handler != null)
                {
                    handler(this, new PropertyChangedEventArgs("Notify"));
                }
            }
        }

        /// <summary>
        /// Test that if Item saves PropertyChanged subsribers
        /// during serialization, then after deserialization
        /// all items corectly report about PropertyChanges 
        /// to collection
        /// </summary>
        [TestMethod]
        public void PropertyChangedSubscriptionAfterDeserialized()
        {
            int addItemCount = 3;
            var list = new List<EventSerializedPropertyChanged>();

            for (int i = 0; i < addItemCount; i++)
            {
                list.Add(new EventSerializedPropertyChanged());
            }

            var observableCollection =
                new ObservableCollectionBase<EventSerializedPropertyChanged, List<EventSerializedPropertyChanged>>(list);
            
            //save & restore
            {
                MemoryStream memory = new MemoryStream();
                BinaryFormatter formatter = new BinaryFormatter();

                formatter.Serialize(memory, observableCollection);

                memory.Seek(0, SeekOrigin.Begin);

                observableCollection =
                    (ObservableCollectionBase<EventSerializedPropertyChanged, List<EventSerializedPropertyChanged>>)
                    formatter.Deserialize(memory);
            }

            Counter callbackCount = new Counter();
            observableCollection.PropertyChanged += (s, e) => callbackCount.count++;

            //verify whenever item notifies PropertyEvent from collection is fired.
            foreach (var item in observableCollection)
            {
                callbackCount.count = 0;
                item.Notify();
                Assert.AreEqual(callbackCount.count, 1);
            }
        }

        /// <summary>
        /// This class does not serialize subscribers for event
        /// </summary>         
        [Serializable]
        class EventNonSerializedPropertyChanged : INotifyPropertyChanged
        {
            [field: NonSerialized]
            public event PropertyChangedEventHandler PropertyChanged;

            public void Notify()
            {
                PropertyChangedEventHandler handler = PropertyChanged;
                if (handler != null)
                {
                    handler(this, new PropertyChangedEventArgs("Notify"));
                }
            }
        }

        /// <summary>
        /// Test that if Item does not save PropertyChanged subsribers
        /// during serialization, then after deserialization
        /// all items corectly report about PropertyChanges 
        /// to collection
        /// </summary>
        [TestMethod]
        public void PropertyChangedSubscriptionAfterDeserialized2()
        {
            int addItemCount = 3;
            var list = new List<EventNonSerializedPropertyChanged>();

            for (int i = 0; i < addItemCount; i++)
            {
                list.Add(new EventNonSerializedPropertyChanged());
            }

            var observableCollection =
                new ObservableCollectionBase<EventNonSerializedPropertyChanged, List<EventNonSerializedPropertyChanged>>(list);

            //save & restore
            {
                MemoryStream memory = new MemoryStream();
                BinaryFormatter formatter = new BinaryFormatter();

                formatter.Serialize(memory, observableCollection);

                memory.Seek(0, SeekOrigin.Begin);

                observableCollection =
                    (ObservableCollectionBase<EventNonSerializedPropertyChanged, List<EventNonSerializedPropertyChanged>>)
                    formatter.Deserialize(memory);
            }

            Counter callbackCount = new Counter();
            observableCollection.PropertyChanged += (s, e) => callbackCount.count++;

            //verify whenever item notifies PropertyEvent from collection is fired.
            foreach (var item in observableCollection)
            {
                callbackCount.count = 0;
                item.Notify();
                Assert.AreEqual(callbackCount.count, 1);
            }
        }

        
    }
}
